/*
 * TMap Designer - A Thematic Map design software
 * Copyright (C) 2006 World Health Organization
 * http://www.who.int
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301  USA
 */
package who.tmap.exp;

import java.io.DataOutputStream;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.math.BigDecimal;
import java.text.SimpleDateFormat;
import java.util.ArrayList;
import java.util.Calendar;
import java.util.Date;
import java.util.List;

import org.apache.commons.io.EndianUtils;
import org.apache.log4j.Logger;

import who.tmap.bo.AttrDef;
import who.tmap.bo.Attrs;
import who.tmap.common.TMapException;
import who.tmap.imp.dbase.DBaseFieldDescriptor;
import who.tmap.imp.dbase.DBaseFieldTypes;
import who.tmap.imp.dbase.DBaseHeader;
import who.tmap.imp.dbase.DBaseObjects;
import who.tmap.imp.dbase.impl.DBaseObjectFactory;

/**
 * A helper to write dBase file
 * File format specifications found at http://www.pgts.com.au/download/public/xbase.htm
 * @author stl
 *
 */
public class DBaseExportAdapter implements ExportAdapter, DBaseFieldTypes {

    private static Logger logger =Logger.getLogger(DBaseExportAdapter.class);
    private static DBaseObjectFactory dbof =DBaseObjectFactory.getInstance();
    private static final SimpleDateFormat sdf = new SimpleDateFormat( "yyyyMMdd" );
    
    private static final byte DBASE_FILE_CODE =3;   
    private static final byte EOF =26;
    private static final byte EOFD =13;  
    private static final byte FIELD_NOT_DELETED_FLAG =32;
    
    private File file;
    private FileOutputStream fos;
    private DataOutputStream dos;      
    
    public DBaseExportAdapter() {
    }
    
    public void export( File file, Attrs attrs ) throws TMapException {
    	try {
    		openFile( file );
    		
    		List<DBaseFieldDescriptor> fieldDescriptors =processFieldDescriptors( attrs );
    		List<List<String>> records =processRecords( attrs, fieldDescriptors );
    		DBaseHeader header =processHeader( attrs, fieldDescriptors );
    		
    		writeFile( header, fieldDescriptors, records );
    	} catch( Exception e ) {
    		logger.error( e );
    	} finally {
    		try {
    			closeFile();
    		} catch( Exception e2 ) {
    			logger.error( e2 );
    		}
    	}
    	
    }
    
    private void writeFile( DBaseHeader header, List<DBaseFieldDescriptor> fieldDescriptors, List<List<String>> records ) throws IOException {
    	writeHeader( header );
    	writeFieldDescriptors( fieldDescriptors );
    	writeRecords( fieldDescriptors, records );
    	
    	// EOF
    	dos.writeByte( EOF );
    }
    
    private void writeHeader( DBaseHeader header ) throws IOException {
    	dos.writeByte( header.getFileCode() );
    	
    	dos.writeByte( header.getYearOfLastUpdate() );
    	dos.writeByte( header.getMonthOfLastUpdate() );
    	dos.writeByte( header.getDayOfLastUpdate() );    	

    	writeInt( header.getNbRecords() );
    	writeShort( header.getHeaderLength() );
    	writeShort( header.getRecordLength() );

    	writeShort( header.getReserved1213() );
    	dos.writeByte( header.getIncompleteTransaction() );
    	dos.writeByte( header.getEncryptionFlag() );
    	
    	writeInt( header.getFreeRecordThread() );
    	writeDouble( header.getMultiUser() );

    	dos.writeByte( header.getMdxFlag() );
    	dos.writeByte( header.getLanguageDriver() );
    	writeShort( header.getReserved3031() );
    }
    
    private void writeFieldDescriptors( List<DBaseFieldDescriptor> fieldDescriptors ) throws IOException {
    	for( DBaseFieldDescriptor desc : fieldDescriptors ) {
    		for( int ct =0; ct < 11; ct++ ) {
    			if ( ct < desc.getName().length() ) {
    				dos.writeByte( desc.getName().charAt( ct ) );
    			} else {
    				dos.writeByte( 0 );
    			}
    		}

    		dos.writeByte( desc.getType().charAt(0) );
    		writeInt( desc.getDataAddress() );
    		dos.writeByte( desc.getLength() );
    		dos.writeByte( desc.getDecimalCount() );
    		writeShort( desc.getMultiUser1819() );
    		dos.writeByte( desc.getWorkAreaId() );
    		writeShort( desc.getMultiUser2122() );
    		dos.writeByte( desc.getSetFields() );
    		writeInt( desc.getReserved24252627() );
    		writeShort( desc.getReserved2829() );
    		dos.writeByte( desc.getReserved30() );
    		dos.writeByte( desc.getIndexFieldFlag() );
    	}
    	
    	// End of field descriptor structure
    	dos.writeByte( EOFD );
    }
    
    private void writeRecords( List<DBaseFieldDescriptor> fieldDescriptors, List<List<String>> records ) throws IOException {
    	for( List<String> rec : records ) {
    		// Delete flag, 32=not deleted
    		dos.writeByte( FIELD_NOT_DELETED_FLAG );
    		
    		for( int ct =0; ct < fieldDescriptors.size(); ct++ ) {
    			String field =rec.get( ct );
    			writeString( field, fieldDescriptors.get( ct ).getLength() );
    		}
    	}
    }
    
    public static File convertShapeFileNameToDBaseFileName( File shapeFile ) throws IOException {
    	String dbName =shapeFile.getCanonicalPath()
    							.substring( 0, shapeFile.getCanonicalPath().length() - 4 );
    	dbName+=".dbf";
    	
    	return new File( dbName );
    }
       
    /**
     * Open the dbase file
     * @param file
     * @throws FileNotFoundException
     */
    public void openFile( File file ) throws FileNotFoundException {
        this.file =file;
        
        fos = new FileOutputStream( file );
        dos = new DataOutputStream( fos );
    }
    
    /**
     * Close the dbase file
     * @throws IOException
     */
    public void closeFile() throws IOException {
        dos.close();
        fos.close();
    }    
    
    private DBaseHeader processHeader( Attrs attrs, List<DBaseFieldDescriptor> descs ) throws TMapException {
    	DBaseHeader header =(DBaseHeader)dbof.get( DBaseObjects.DBASE_HEADER );
    	
    	header.setFileCode( DBASE_FILE_CODE );
    	
    	Calendar now =Calendar.getInstance();
    	header.setDayOfLastUpdate( (byte)now.get( Calendar.DATE ) );
    	header.setMonthOfLastUpdate( (byte)now.get( Calendar.MONTH ) );
    	header.setYearOfLastUpdate( (byte)(now.get( Calendar.YEAR ) - 2000) );
    	
    	header.setNbRecords( attrs.getRows().size() );
    	header.setHeaderLength( (short)( (descs.size() + 1) * 32 + 1 ) );
    	header.setRecordLength( getRecordLength( descs ) );
    	
    	return header;
    }
    
    private short getRecordLength( List<DBaseFieldDescriptor> descs ) {
    	 // 1 for delete flag
    	int length =1;
    	
    	for( DBaseFieldDescriptor desc : descs ) {
    		length+=desc.getLength();
    	}
    	
    	return (short)length;
    }
    
    private List<DBaseFieldDescriptor> processFieldDescriptors( Attrs attrs ) throws Exception {
    	List<DBaseFieldDescriptor> descs =new ArrayList<DBaseFieldDescriptor>();
    	
    	for( AttrDef attrDef : attrs.getHeader() ) {
    		DBaseFieldDescriptor desc =(DBaseFieldDescriptor)dbof.get( DBaseObjects.DBASE_FIELD_DESCRIPTOR );
    		descs.add( desc );
    		
    		String name =attrDef.getName().trim();
    		if ( name.length() > 10 ) {
    			name =name.substring( 0, 10 );
    			logger.warn( "truncating attr name [" + name + "] to 10 characters." );
    		} else if ( name.length() < 10 ) {
    			for( int charCt =name.length(); charCt <= 10; charCt++ ) {
    				name+=(char)0;
    			}
    		}
    		desc.setName( name );
    		
    		byte decimalCount =0;
    		byte length =0;
    		
    		char type;
    		
    		if ( String.class.getName().equals( attrDef.getType() ) ) {
    			type =CHARACTER;
    			
    		} else if ( BigDecimal.class.getName().equals( attrDef.getType() ) 
            		|| Float.class.getName().equals( attrDef.getType() )
            		|| Double.class.getName().equals( attrDef.getType() )
            		) {
                type =FLOATING_POINT;
                
            } else if ( Boolean.class.getName().equals( attrDef.getType() ) ) {
                type=LOGICAL;
                length =1;
                
            } else if ( Integer.class.getName().equals( attrDef.getType() ) 
            		|| Long.class.getName().equals( attrDef.getType() )
            		|| Short.class.getName().equals( attrDef.getType() )
            		|| Byte.class.getName().equals( attrDef.getType() )
            		) {
                type =NUMBER;
                
            } else if ( Date.class.getName().equals( attrDef.getType() ) ) {
            	type =DATE;
            	length =8;
            	
            } else {
        		// String by default
        		type =CHARACTER;
        	}
            
            desc.setType( String.valueOf( type ) );
            desc.setDecimalCount( decimalCount );
            desc.setLength( length );
            
    	}
    	
    	return descs;
    }
    
	@SuppressWarnings("unchecked")
	List<List<String>> processRecords( Attrs attrs, List<DBaseFieldDescriptor> fieldDescriptors ) {
		List<List<String>> rows =new ArrayList<List<String>>();
		
		for( List attrRow : attrs.getRows() ) {
			List<String> row =new ArrayList<String>();
			rows.add( row );
			
			for( int fieldCt =0; fieldCt < attrRow.size(); fieldCt++ ) {
				
				DBaseFieldDescriptor desc =fieldDescriptors.get( fieldCt );
				
				Object val =attrRow.get( fieldCt );
				
				String strVal ="";
				
				if ( CHARACTER == desc.getType().charAt(0) ) {
					strVal =val != null ? val.toString() : "";
					
					// 255 characters max					
					if ( strVal.length() > 254 ) {
						strVal=strVal.substring(0, 254);
					}
					
					if ( strVal.length() > desc.getLength() ) {
						desc.setLength( (byte)strVal.length() );
					}
					
				} else if ( DATE == desc.getType().charAt(0) ) {
	                strVal =val != null ? sdf.format( (Date)val ) : "19700101";
	                
				} else if ( NUMBER == desc.getType().charAt(0) ) {
					strVal =val != null ? val.toString() : "0";
					
					// 18 characters max					
					if ( strVal.length() > 18 ) {
						strVal=strVal.substring(0, 18);
					}
					
					if ( strVal.length() > desc.getLength() ) {
						desc.setLength( (byte)strVal.length() );
					}
				} else if ( FLOATING_POINT == desc.getType().charAt(0) ) {
					strVal =val != null ? val.toString() : "0";
					
					// 20 characters max					
					if ( strVal.length() > 20 ) {
						strVal=strVal.substring(0, 20);
					}
					
					if ( strVal.length() > desc.getLength() ) {
						desc.setLength( (byte)strVal.length() );
					}
					
					// deal with decimal part later
					
				} else if ( LOGICAL == desc.getType().charAt( 0 ) ) {
					strVal =val == null || ( (Boolean)val ) ? "Y" : "N";
				}

				row.add( strVal );
			}
		}
		
		return rows;
	}
   
    /**
     * Writes a little endian short
     * @return little endian short
     * @throws IOException
     */
    private void writeShort( short val ) throws IOException
    {
    	EndianUtils.writeSwappedShort( dos, val );
    }    
    
    /**
     * Writes a little endian int
     * @return little endian int
     * @throws IOException
     */
    private void writeInt( int val ) throws IOException
    {
    	EndianUtils.writeSwappedInteger( dos, val );
    }
    
    /**
     * Writes a little endian double
     * @return little endian double
     * @throws IOException
     */
    private void writeDouble( double val ) throws IOException
    {
    	EndianUtils.writeSwappedDouble( dos, val );
    }       
    
    /**
     * Writes a String
     * @param str
     * @param fieldLength
     * @throws IOException
     */
    private void writeString( String str, int fieldLength ) throws IOException {
		for( int ct =0; ct < fieldLength; ct++ ) {
			dos.writeByte( ct < str.length() ? str.charAt( ct ) : 32 );	
		}
    }

}
